﻿using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using Flowertwig.Utils.Connections.Interfaces;
using Flowertwig.Utils.Entities.Security;
using Flowertwig.Utils.Enums;
using Flowertwig.Utils.Events;
using Flowertwig.Utils.Exceptions;

namespace Flowertwig.Utils.Connections
{
    /// <summary>
    /// Class represents a Tcp/Ip connection supporting diffrent protocols
    /// </summary>
    public abstract class TcpConnection : System.IDisposable
    {
        public const int Connecting = 0;
        public const int Connected = 1;
        public const int Disconnected = 2;

#if !COMPACT_FRAMEWORK
        /// <summary>
        /// Used when validating a remote certificate in a secure connection.
        /// object is CertificateValidationInfo
        /// </summary>
        public const int SecurityValidateRemoteCertificate = 1000;
        /// <summary>
        /// Used to select Certificate to use in a secure connection.
        /// object is LocalCertificationSelectionInfo
        /// </summary>
        public const int SecuritySelectLocalCertificate = 1001;
        /// <summary>
        /// Used when a known error occured in the authentication phase (With a secure connection)
        /// object is a dervided class of Exception
        /// </summary>
        public const int SecurityAuthenticationError = 1002;
#endif


        #region Events
        /// <summary>
        /// Is triggered when protocol has been changed.
        /// Data is the new Protocol.
        /// </summary>
        public event EventHandler ProtocolChange;
        /// <summary>
        /// Is triggered when connection status has been changed.
        /// Action is:
        /// 0 = Connecting
        /// 1 = Connected
        /// 2 = Disconnected
        /// </summary>
        public event EventHandler ConnectionStatusChange;
#if !COMPACT_FRAMEWORK
        // Security
        public event EventHandler SecureUpdate;
#endif
        #endregion
        #region Variables
        protected bool importedSocket;
        protected IPEndPoint remoteAddress;
        protected IPEndPoint localAddress;
        protected Socket socket;
        protected byte[] buffer = new byte[1048576];
        protected IProtocol protocol;
        protected bool first = true;
        protected bool disposed;
        protected bool disposing;
        protected bool shouldBlockSending;
        protected bool isBlockingSending;

        public bool ShouldBlockOnSend
        {
            get { return shouldBlockSending; }
            set { shouldBlockSending = value; }
        }

#if !COMPACT_FRAMEWORK
        // Security
        protected SslStream _secStream;
        protected SecurityProtocols _secProtocol = SecurityProtocols.None;
#endif

        // Thread signal, It is needed so we know when we have a connection.
        protected System.Threading.ManualResetEvent allDone = new System.Threading.ManualResetEvent(false);

        #endregion
        #region Properties

#if !COMPACT_FRAMEWORK
        // Security
        public SslStream SecureStream
        {
            get { return _secStream; }
            set { _secStream = value; }
        }

        public SecurityProtocols SecureProtocol
        {
            get { return _secProtocol; }
            set
            {
                _secProtocol = value;
                switch (_secProtocol)
                {
                    case SecurityProtocols.None:
                        if (_secStream != null)
                            _secStream.Close();
                        _secStream = null;
                        socket.Blocking = false;
                        break;
                    default:
                        if (socket == null || !socket.Connected)
                            return;
                        CheckSecure();
                        break;
                }
            }
        }


        protected void CheckSecure()
        {
            try
            {
                if (_secProtocol != SecurityProtocols.None && _secStream == null)
                {
                    // This is ugly as hell! :p
                    // TODO : Make this less ugly
                    var client = new TcpClient();
                    socket.Blocking = true;
                    client.Client = socket;
                    _secStream = new SslStream(
                        client.GetStream(),
                        false,
                        OnRemoteCertificateValidation,
                        OnLocalCertificateSelection
                        );

                    SslProtocols protocol;
                    switch (_secProtocol)
                    {
                        case SecurityProtocols.SSL2:
                            protocol = SslProtocols.Ssl2;
                            break;
                        case SecurityProtocols.SSL3:
                            protocol = SslProtocols.Ssl3;
                            break;
                        case SecurityProtocols.TLS:
                            protocol = SslProtocols.Tls;
                            break;
                        default:
                            throw new System.NotSupportedException("This protocol '" + _secProtocol + "' is not supported");
                    }

                    if (importedSocket)
                        //secStream.AuthenticateAsServer(new X509Certificate(), true, protocol, true);
                        _secStream.AuthenticateAsServer(new X509Certificate(), false, protocol, false);
                    else
                        //secStream.AuthenticateAsClient("", new X509CertificateCollection(), protocol, true);
                        _secStream.AuthenticateAsClient("", new X509CertificateCollection(), protocol, false);
                }
            }
            catch (AuthenticationException ae)
            {
                SecureUpdate(this, new DefaultEventArgs(SecurityAuthenticationError, ae));
            }
            catch (System.PlatformNotSupportedException pe)
            {
                SecureUpdate(this, new DefaultEventArgs(SecurityAuthenticationError, pe));
            }
            catch (System.IO.IOException ioe)
            {
                SecureUpdate(this, new DefaultEventArgs(SecurityAuthenticationError, ioe));
            }
        }
#endif
        public bool IsDisposed
        {
            get { return disposed; }
        }

        public bool IsDisposing
        {
            get { return disposing; }
        }

        /// <summary>
        /// Protocol that is beeing used for this connection.
        /// </summary>
        public IProtocol Protocol
        {
            get { return protocol; }
            set
            {
                IProtocol tmp = protocol;
                protocol = value;
                // Trigger Protocol Changed Event.
                var e = new DefaultEventArgs(0, tmp);
                ProtocolChange(this, e);
                if (!e.Handled && tmp != null)
                {
                    // If not marked as handled. Dispose protocol
                    tmp.Dispose();
                }
            }
        }
        /// <summary>
        /// Remote IPEndPoint for the other party
        /// </summary>
        public IPEndPoint RemoteAddress
        {
            get { return remoteAddress; }
        }
        /// <summary>
        /// Local IPEndPoint for our party
        /// </summary>
        public IPEndPoint LocalAddress
        {
            get
            {
                if (localAddress == null && socket != null)
                    return (IPEndPoint)socket.LocalEndPoint;
                return localAddress;
            }
            set { localAddress = value; }
        }

        #endregion
        #region Constructor(s)/Deconstructor/Dispose
        /// <summary>
        /// Creating TcpConnection
        /// </summary>
        protected TcpConnection()
        {
            #region Event(s)
            ProtocolChange = new EventHandler(OnProtocolChanged);
            ConnectionStatusChange = new EventHandler(OnConnectionStatusChanged);
#if !COMPACT_FRAMEWORK
            // Security
            SecureUpdate = new EventHandler(OnSecureUpdate);
#endif
            #endregion
        }

        /// <summary>
        /// Creating TcpConnection with s as the underlaying socket
        /// </summary>
        /// <param name="s">Socket you want to have as underlaying connection</param>
        protected TcpConnection(Socket s)
            : this()
        {
            socket = s;
            importedSocket = true;
        }

        /// <summary>
        /// Creating TcpConnection with addy as the remote address.
        /// </summary>
        /// <param name="addy">IPEndPoint that we want as our remote address</param>
        protected TcpConnection(IPEndPoint addy)
            : this()
        {
            remoteAddress = addy;
        }

        /// <summary>
        /// Creating TcpConnection with address and prt.
        /// </summary>
        /// <param name="address">string representation of a IP/DNS address</param>
        /// <param name="prt">int port representation</param>
        protected TcpConnection(string address, int prt)
            : this()
        {
            IPAddress addy;
            try
            {
                //addy = System.Net.Dns.GetHostEntry(address).AddressList[0];
                addy = IPAddress.Parse(address);
            }
            catch (System.Exception)
            {
                // We are not going to try to catch this as developer that used this class made something wrong if this has to be thrown.
                //addy = System.Net.IPAddress.Parse(address);
                addy = Dns.GetHostEntry(address).AddressList[0];
            }
            remoteAddress = new IPEndPoint(addy, prt);
        }

        /// <summary>
        /// Deconstructor
        /// </summary>
        ~TcpConnection()
        {
            Dispose();
        }

        public virtual void Dispose()
        {
            if (!disposed)
            {
                disposing = true;
                Disconnect("Dispose");

                allDone.Close();
                buffer = null;
                localAddress = null;
                remoteAddress = null;

                ProtocolChange -= OnProtocolChanged;
                // ConnectionStatusChange need to be removed after calling Disconnect as Disconnect fires ConnectionStatusChange;
                ConnectionStatusChange -= OnConnectionStatusChanged;
#if !COMPACT_FRAMEWORK
                SecureUpdate -= OnSecureUpdate;
#endif
                if (socket != null)
                {
                    socket.Close();
                    socket = null;
                }
                if (protocol != null)
                {
                    protocol.Dispose();
                    protocol = null;
                }
#if !COMPACT_FRAMEWORK
                if (_secStream != null)
                {
                    _secStream.Dispose();
                    _secStream = null;
                }
#endif
                System.GC.SuppressFinalize(this);
                disposed = true;
                disposing = false;
            }
        }

        #endregion
        #region Functions
        #region Connect
        /// <summary>
        /// Creates connection to server.
        /// </summary>
        public virtual void Connect()
        {
            // Change Connection status.
            ConnectionStatusChange(this, new DefaultEventArgs(Connecting));
            first = true;
            if (Protocol == null)
                throw new System.NullReferenceException("Protocol is null, You need to set Protocol before trying to connect.");
            try
            {
                // Establish Connection
                socket = new Socket(remoteAddress.AddressFamily
                    , SocketType.Stream
                    , ProtocolType.Tcp);
                // It is needed so we know when we have a connection.
                allDone.Reset();
                socket.Blocking = false;
                System.AsyncCallback onconnect = OnConnect;
                socket.BeginConnect(remoteAddress, onconnect, socket);
                // Waits until we have a connection...
                allDone.WaitOne();
            }
            catch (System.Exception e2)
            {
                // Change Connection Status
                ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, e2));
                return;
            }
        }

        /// <summary>
        /// Disconnects connection
        /// </summary>
        public void Disconnect()
        {
            Disconnect(null);
        }

        /// <summary>
        /// Disconnects connection
        /// </summary>
        /// <param name="msg">Message that will be sent out in the ConnectionStatusChange event</param>
        public virtual void Disconnect(string msg)
        {
            try
            {
                ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, new FlowertwigException(msg)));

#if !COMPACT_FRAMEWORK
                if (_secStream != null)
                {
                    _secStream.Close();
                    _secStream = null;
                }
#endif

                //socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger,new LingerOption(false, 0));
                socket.Disconnect(true);
            }
            catch { }
        }

        protected virtual void OnConnect(System.IAsyncResult ar)
        {
            // We have a working connection.. :)
            allDone.Set();

            // Socket was the passed in object
            var sock = (Socket)ar.AsyncState;
            // Check if we were sucessfull
            try
            {
                sock.EndConnect(ar);
                if (sock.Connected)
                {
                    // Change Connection Status
                    ConnectionStatusChange(this, new DefaultEventArgs(Connected));

#if !COMPACT_FRAMEWORK
                    CheckSecure();
#endif

                    SetupRecieveCallback(sock);
                }
                else
                {
                    // Change Connection Status
                    ConnectionStatusChange(this, new DefaultEventArgs(Disconnected));
                }
            }
            catch (System.ObjectDisposedException) { }
            catch (SocketException se)
            {
                // Change Connection Status
                ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, se));
            }
        }
        #endregion
        #region Receive
        /// <summary>
        /// Setup the callback for recieved data and loss of conneciton
        /// </summary>
        protected virtual void SetupRecieveCallback(Socket sock)
        {
            try
            {
                if (sock.Connected)
                {
                    // Determin Protocol to use here.
                    //if (Protocol != null && sock.Connected && first)
                    if (first && !importedSocket && Protocol != null)
                    {
                        first = false;
                        if (Protocol.FirstCommand != null)
                            Send(Protocol.FirstCommand);
                    }
                    var recieveData = new System.AsyncCallback(OnRecievedData);
#if !COMPACT_FRAMEWORK
                    if (_secStream != null)
                    {
                        try
                        {
                            _secStream.BeginRead(buffer, 0, buffer.Length, recieveData, _secStream);
                        }
                        catch (System.IO.IOException ioex)
                        {
                            // Change Connection Status
                            ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, ioex));
                        }
                    }
                    else
#endif
                        sock.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, recieveData, sock);
                }
            }
            catch (System.ObjectDisposedException) { }
            catch (System.ArgumentNullException ane)
            {
                // Change Connection Status
                ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, ane));
            }
            catch (SocketException se)
            {
                // Change Connection Status
                ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, se));
            }
        }
        /// <summary>
        /// Get the new data and send it out to all other connections. 
        /// Note: If no data was recieved the connection has probably 
        /// died.
        /// </summary>
        /// <param name="ar"></param>
        protected virtual void OnRecievedData(System.IAsyncResult ar)
        {
            // Socket was the passed in object
            var sock = ar.AsyncState as Socket;
            int nBytesRec;
            // Check if we got any data
            try
            {
#if !COMPACT_FRAMEWORK
                if (sock == null)
                {
                    var handler = ar.AsyncState as SslStream;
                    try
                    {
                        nBytesRec = handler.EndRead(ar);
                    }
                    catch (System.IO.IOException ioex)
                    {
                        // Change Connection Status
                        ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, ioex));
                        //System.Console.WriteLine("OnRecieveData:" + ioex.ToString());
                        return;
                    }
                    // TODO : REMOVE
                    sock = socket;
                }
                else
#endif
                    nBytesRec = sock.EndReceive(ar);
                if (nBytesRec > 0)
                {
                    // Send data to protocol.
                    Protocol.ParseRaw(buffer, nBytesRec);

                    // If the connection is still usable restablish the callback
                    SetupRecieveCallback(sock);
                }
                else
                {
                    // If no data was recieved then the connection is probably dead
                    // Change Connection Status
                    ConnectionStatusChange(this, new DefaultEventArgs(Disconnected));
                }
            }
            catch (System.ObjectDisposedException) { }
            catch (SocketException ex)
            {
                if (ex.ErrorCode != 10057)
                {
                    // Change Connection Status
                    ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, ex));
                }
            }
        }
        #endregion
        /// <summary>
        /// If existing connection exist it will be closed.
        /// Then a new connection attempt will be made
        /// </summary>
        public virtual void Reconnect()
        {
            // If connected, Disconnect.
            if (socket != null && socket.Connected)
                ConnectionStatusChange(this, new DefaultEventArgs(Disconnected));
            // Connecting.
            Connect();
        }

        /// <summary>
        /// Sets TcpConnection in a listening mode waiting for a message.
        /// NOTE: This function can't be used if you havnt used the TcpConnection(Socket s) constructor.
        /// </summary>
        public virtual void Listen()
        {
            if (!importedSocket)
                throw new System.InvalidOperationException("To call this function you need to have created this object with the TcpConnection(Socket s) constructor");
            if (socket.Connected)
            {
                localAddress = (IPEndPoint)socket.LocalEndPoint;
                remoteAddress = (IPEndPoint)socket.RemoteEndPoint;

#if !COMPACT_FRAMEWORK
                CheckSecure();
#endif
                SetupRecieveCallback(socket);
                // Change Connection Status
                ConnectionStatusChange(this, new DefaultEventArgs(Connected));
            }
        }

        #region Send
        /// <summary>
        /// Sends Raw from msg to server.
        /// </summary>
        /// <param name="msg">Message where </param>
        public virtual void Send(IConMessage msg)
        {
            if (Protocol.OnSend(msg))
            {
                Send(msg.Bytes);
            }
        }
        /// <summary>
        /// Sends byte[] to server.
        /// </summary>
        /// <param name="raw">byte[] that will be sent to server</param>
        public virtual void Send(byte[] raw)
        {
            if (socket == null || !socket.Connected)
            {
                //System.Console.WriteLine("OnSend: Not connected");
                return;
            }
            try
            {
                if (raw == null || raw.Length <= 0)
                {
                    //System.Console.WriteLine("OnSend:Raw is empty");
                    return;
                }

                // This is so we block file transfers
                if (shouldBlockSending)
                    isBlockingSending = true;

                // Some how Send doesnt work on my Pocket PC. Because of this we use BeginSend // Flow84
                System.AsyncCallback sendData = OnSendData;
#if !COMPACT_FRAMEWORK
                if (_secStream != null)
                {
                    try
                    {
                        // TODO : Why cant we use async write here?! :S
                        _secStream.Write(raw, 0, raw.Length);
                        //secStream.BeginWrite(raw, 0, raw.Length, sendData, secStream);
                    }
                    catch (System.IO.IOException ioex)
                    {
                        // Change Connection Status
                        ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, ioex));
                        //System.Console.WriteLine("OnSend:" + ioex.ToString());
                        return;
                    }
                }
                else
#endif
                    socket.BeginSend(raw, 0, raw.Length, SocketFlags.None, sendData, socket);


                // This is so we block file transfers
                if (shouldBlockSending)
                {
                    while (isBlockingSending)
                    {
                        //System.Console.WriteLine("OnSend: Is blocking sending");
                        System.Threading.Thread.Sleep(100);
                    }
                    //System.Console.WriteLine("OnSend: Not blocking anymore");
                }
            }
            catch (System.ObjectDisposedException) { }
            catch (SocketException se)
            {
                if (se.ErrorCode != 10057)
                {
                    // Change Connection Status
                    ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, se));
                }
            }
        }
        protected virtual void OnSendData(System.IAsyncResult async)
        {

            var handler = async.AsyncState as Socket;
            try
            {
#if !COMPACT_FRAMEWORK
                if (handler == null)
                {
                    var handlerS = async.AsyncState as SslStream;
                    handlerS.EndWrite(async);
                }
                else
#endif
                {
                    handler.EndSend(async);
                }
                if (shouldBlockSending)
                {
                    isBlockingSending = false;
                }
            }
            catch (System.ObjectDisposedException) { }
            catch (SocketException se)
            {
                // Change Connection Status
                ConnectionStatusChange(this, new DefaultEventArgs(Disconnected, se));
            }
        }
        #endregion
        protected virtual void OnProtocolChanged(object sender, DefaultEventArgs e)
        {

        }
        protected virtual void OnConnectionStatusChanged(object sender, DefaultEventArgs e)
        {

        }


#if !COMPACT_FRAMEWORK
        // Secure
        void OnSecureUpdate(object sender, DefaultEventArgs e) { }
        protected bool OnRemoteCertificateValidation(
              object sender,
              X509Certificate certificate,
              X509Chain chain,
              SslPolicyErrors sslPolicyErrors)
        {
            var info = new CertificateValidationInfo(certificate, chain, sslPolicyErrors);
            var args = new DefaultEventArgs(SecurityValidateRemoteCertificate, info);
            SecureUpdate(this, args);
            info = args.Data as CertificateValidationInfo;
            if (args.Handled)
            {
                if (info == null)
                    return false;
                return info.Accepted;
            }
            // If developer havnt added logic to support this. Assume Accepted.
            return true;
        }

        protected X509Certificate OnLocalCertificateSelection(
            object sender,
            string targetHost,
            X509CertificateCollection localCertificates,
            X509Certificate remoteCertificate,
            string[] acceptableIssures)
        {
            var info = new LocalCertificationSelectionInfo(targetHost, localCertificates, remoteCertificate, acceptableIssures);
            var args = new DefaultEventArgs(SecuritySelectLocalCertificate, info);
            SecureUpdate(this, args);
            info = args.Data as LocalCertificationSelectionInfo;
            if (info == null)
                return null;
            return info.SelectedCertificate;
        }
#endif

        #endregion
    }
}